\section{C++流}
\begin{itemize}
    \item cin对象对应于标准输入流。在默认情况下，这个流被关联到标准输入设备（通常为键盘）。wcin对象与其类似，但是处理的是wchar\_t类型。
    \item cout对象和标准输出流对应，在默认情况下，这个流被关联到标准输出设备（通常为显示器）。wcout对象与此类似，但处理的是wchar\_t类型。
    \item cerr对象与标准错误流相对应，可用于显示错误信息。默认情况下这个流被关联到标准输出设备（通常为显示器）。这个流没有被缓冲，这意味着信息将被直接发送给屏幕，而不会等到缓冲区填满或者新的换行符。werr对象与此类似，但处理的是wchar\_t类型。
    \item clog对象也对应于标准错误流。在默认情况下，这个流被关联到标准输出设备（通常为显示器）。这个流被缓冲。wlog对象与此类似，但是处理的是wchar\_t类型。
    \item 对象代表流——这意味着什么？当ios文件为程序声明一个cout对象的时候，该对象将包含存储了和输出有关的信息的数据成员，如果显示数据时使用的字段宽度，小数位数，显示整数时采用的计数方法以及描述用来处理输出流的缓冲区的streambuf对象的地址。下面的语句通过指向的streambuf对象将字符串"apple"放在cout管理的缓冲区中：\\
          \cppinline{cout<<"apple"}
\end{itemize}
\subsection{使用cout进行输出}
\begin{itemize}
    \item C++使用指向字符串存储位置的指针来表示字符串。指针的形式可以是\textcolor{red}{char数据名}，\textcolor{blue}{显式的char指针}，\textcolor{green}{用引号扩起的字符串}。下面的语句都是正确的：
          \begin{cpp}
              char name[20] = "hello world";
              char *pn = "hello world";
              cout<<name;
              cout<<pn;
          \end{cpp}
          方法使用字符串中的空字符来确定何时使用停止显示字符。对于其他类型的指针，C++将其对应于void *，并打印地址的数值显示。如果要获得字符串的地址则必须将其强制转化为其它类型，如下面的代码：
          \begin{cpp}
              int eggs = 12;
              char * amount = "dozen"; // 打印eggs的地址
              cout<<&eggs;
              cout<<amount;          //打印”dozen“
              void<<(void *) amount; //打印”dozen“的地址
          \end{cpp}
    \item 拼接输出，插入运算符的所有的返回类型都是ostream \&,原型的格式如下：\\
          \cppinline{ostream & operator<<(type);}(type是要显示的数据类型)返回类型ostream \&意味着使用该运算符将返回一个指向ostream对象的引用。
\end{itemize}
\subsection{ostream的方法}
\begin{itemize}
    \item put用于显示字符。put方法的原型：\cppinline{ostream & put(char)} 当前标准与之相同，但是被模板化了以适用于wchar\_t,\\
          \cppinline{cout.put('w')}cout是调用方法的对象，put()是类的成员函数。在原型合适的情况下可以将数值型参数（如int）用于put（），让函数原型自动将参数转换为正确的char值。例如可以这样：
          \begin{cpp}
              cout.put(65) //显示字符A（ASCII 65）
              cout.put(66.3) //显示字符B（ASCII 66）

          \end{cpp}
    \item write()方法显示字符串，函数模板原型如下：\\
          \cppinline{basic_ostream<char T,trains>& write(const char type* s,streamsize n)},第一个参数提供了要显示的字符串的地址，第二个参数指出使用多少个字符
\end{itemize}
\cppfile{code/stream/ex1/main.cpp}
\begin{bash}
    M
    Mi
    Mic
    Micr
    Micro
    Micros
    Microso
    Microsof
    Microsoft
    Microsoft
    Microsof
    Microso
    Micros
    Micro
    Micr
    Mic
    Mi
    M
    Apple Goo
\end{bash}
write方法不会在遇到空字符时自动停止打印字符，而是打印指定数目的字符，即使超过了字符串的边界。

如果实现不能在希望时刷新输出，可以使用两个控制符中的一个强制刷新，控制符flush刷新缓冲区，而控制符endl刷新缓冲区同时插入一个换行符。

\begin{cpp}
    cout<<"Hello,good-looking!"<<flush;
    cout<<"Wait just a moment,please""<<endl;
\end{cpp}
事实上，控制符也是函数，例如，可以直接调用flush()刷新cout缓冲区：\cppinline{flush(cout)}\\
然而,ostream类对\bashinline{<<}插入运算符进行了重载，使得下面的表达式将被替换为函数调用flush(cout)：
\cppinline{cout<<flush}
\subsection{修改显示时使用的计数系统}
设置使用hex，oct，dec，例如设置十六进制输出\cppinline{hex(cout);}完成上述设置后，程序将以16进制形式打印整数值。ostream重载了\bashinline{<<}运算符，这使得上述用法和函数调用hex(cout)等价。
\subsection{调整字段宽度}
\begin{cpp}
    int width() //返回字段宽度的当设置
    int width(int i) //将字段宽度设置为i个空格
\end{cpp}
width只影响下一个项目，然后字段宽度将恢复为默认值。C++永远不会截断数据，因此如果试图在宽度为2的字段中打印7位值，C++将增宽字段，以容纳该数据。
\cppfile{code/stream/ex1/main.cpp}
\begin{bash}
    默认宽度为:
    i*i = 0
    i*i = 1
    i*i = 4
    i*i = 9
    i*i = 16
    i*i = 25
    i*i = 36
    i*i = 49
    i*i = 64
    i*i = 81
\end{bash}
在上述输出中，值在字段中右对齐(默认)。输出中包含空格，也就是说，cout通过加入空格来填满整个字段。右对齐时，空格被插入到值的左侧。

\subsection{填充字符}
默认情况下，cout使用空格填充字段中没有被使用的部分，可以使用fill成员函数改变填充字符，下面的函数调用将填充字符改为星号： \cppinline{cout.fill('*')} 与字段宽度不同的是，新的填充符将一直有效，直到更改它为止。
\subsection{设置浮点数的显示精度}
浮点数精度的函数取决于输出模式。在默认模式下，他指的是显示的总位数。在定点模式和科学计数法模式下，精度指的是小数点后面的位数。已经知道C++默认的精度为6位（但末尾的0将不显示）。precision()成员使得能够选择其它值。例如下面的语句将cout的精度设置为2（设置一直有效）： \cppinline{cout.precision(2)}
\subsection{打印末尾的0和小数点}
ios\_base类提供了一个setf()函数，能够控制多种格式化特性。这个类还定义了多个常量，可用作该函数的参数。下面的调用时cout显示末尾小数点：\mint{cpp}{cout.setf(ios_base::showpoint);}setf函数有两个原型。第一个为： \cppinline{fmtflags setf(fmtflags)} 。 其中fmtflags是bitmask类型的typedef名，用于存储格式标记。该名称是在ios\_base类中定义的。这个版本的setf()用来设置单个位控制的格式信息。参数是一个fmtflags值，指出要设置哪一位。返回值是类型为fmtflags的数字，指出所有标记以前的设置。如果打算恢复原始设置可以保存这个值。常见的参数和含义如下\ref{table:st_1}。
\begin{table}[!ht]
    \caption{格式常量}
    \centering
    \begin{tabular}{ll}
        \toprule
        常量                 & 含义                              \\
        \midrule
        ios\_base::boolalpha & 输入输出bool值，可以为true和false \\
        ios\_base::showbase  & 对于输出，使用C++基数前缀(0,0x)   \\
        ios\_base::showpoint & 显示末尾小数点                    \\
        ios\_base::uppercase & 对16进制输出使用大写字母，E表示法 \\
        ios\_base::showpos   & 在正数前加上+                     \\
        \bottomrule
    \end{tabular}
    \label{table:st_1}
\end{table}

\subsection{流的相关概念}

\begin{figure}[!htbp]
    \centering
    \includegraphics[width=0.5\textwidth]{stream_struct}
    \caption{C++流继承图}
    \label{fig:stream_struct}
\end{figure}
\figref{fig:stream_struct}为流类的继承关系图。为了避免多继承的二义性，从 ios 派生出 istream 和 ostream 时，均使用了 virtual 关键字（虚继承）。
\begin{itemize}
    \item istream 是用于输入的流类，cin 就是该类的对象。
    \item ostream 是用于输出的流类，cout 就是该类的对象。
    \item ifstream 是用于从文件读取数据的类。
    \item ofstream 是用于向文件写入数据的类。
    \item iostream 是既能用于输入，又能用于输出的类。
    \item fstream 是既能从文件读取数据，又能向文件写入数据的类。
\end{itemize}

ostream 类的无参构造函数和拷贝构造函数都是私有的，因此在程序中一般无法定义 ostream 类的对象，唯一能用的 ostream 类的对象就是 cout。当然，上面关于 cout 对象的定义语句说明 ostream 类是有一个公有构造函数的，如果知道这个构造函数是什么样的，那么也能自己定义 ostream 类的对象——但这么做并无必要。cout 可以被重定向，而 cerr 不能。所谓重定向，就是将输入的源或输出的目的地改变。例如，cout 本来是输出到屏幕上的，但是经过重定向，本该输出到屏幕上的东西就可以被输出到文件中。
\cppfile{code/stream/ex4/main.cpp}
上面代码中输出部分将被重定向到文件test中。即文件中的内容为标准数据stdin（键盘）输入的元素和他们的和。
\begin{remark}
    cerr不会被定向到文件中，因为cerr直接在显示器输出，不缓存。
\end{remark}
\subsection{设置流格式}
有时希望按照一定的格式进行输出，如按十六进制输出整数，输出浮点数时保留小数点后面两位，输出整数时按 6 个数字的宽度输出，宽度不足时左边补 0，等等。C语言中的 printf() 函数使用以\%开头的格式控制符，例如 \%X、\%.2f、\%6d 等；C++ 中的 cout 对象则使用流操作算子（你也可以叫做格式控制符）或者成员函数进行控制。

C++ 中常用的输出流操纵算子如\tabref{tab:stream_op} 所示，它们都是在头文件 iomanip 中定义的；要使用这些流操纵算子，必须包含该头文件。
\begin{remark}
    “流操纵算子”一栏中的星号*不是算子的一部分，星号表示在没在默认情况下，整数是用十进制形式输出的，等效于使用了 dec 算子。
\end{remark}
\begin{table}[!htbp]
    \centering
    \caption{流控制}
    \begin{tabular}{p{0.2\textwidth}p{0.8\textwidth}}
        \toprule
        流操纵算子          & 作  用                                                                                                                                                                                                                                                                           \\
        \midrule
        *dec                & 以十进制形式输出整数                                                                                                                                                                                                                                                             \\
        hex                 & 以十六进制形式输出整数                                                                                                                                                                                                                                                           \\
        oct                 & 以八进制形式输出整数                                                                                                                                                                                                                                                             \\
        fixed               & 以普通小数形式输出浮点数                                                                                                                                                                                                                                                         \\
        scientific          & 以科学计数法形式输出浮点数                                                                                                                                                                                                                                                       \\
        left                & 左对齐，即在宽度不足时将填充字符添加到右边                                                                                                                                                                                                                                       \\
        *right              & 右对齐，即在宽度不足时将填充字符添加到左边                                                                                                                                                                                                                                       \\
        setbase(b)          & 设置输出整数时的进制，b=8、10 或 16                                                                                                                                                                                                                                              \\
        setw(w)             & 指定输出宽度为 w 个字符，或输人字符串时读入 w 个字符                                                                                                                                                                                                                             \\
        setfill(c)          & 在指定输出宽度的情况下，输出的宽度不足时用字符 c 填充（默认情况是用空格填充）                                                                                                                                                                                                    \\
        setprecision(n)     & 设置输出浮点数的精度为 n。在使用非 fixed 且非 scientific 方式输出的情况下，n 即为有效数字最多的位数，如果有效数字位数超过 n，则小数部分四舍五人，或自动变为科学计 数法输出并保留一共 n 位有效数字。在使用 fixed 方式和 scientific 方式输出的情况下，n 是小数点后面应保留的位数。 \\
        setiosflags(flag)   & 将某个输出格式标志置为 1                                                                                                                                                                                                                                                         \\
        resetiosflags(flag) & 将某个输出格式标志置为 0                                                                                                                                                                                                                                                         \\
        boolapha            & 把 true 和 false 输出为字符串    不常用                                                                                                                                                                                                                                          \\
        *noboolalpha        & 把 true 和 false 输出为 0、1                                                                                                                                                                                                                                                     \\
        showbase            & 输出表示数值的进制的前缀                                                                                                                                                                                                                                                         \\
        *noshowbase         & 不输出表示数值的进制.的前缀                                                                                                                                                                                                                                                      \\
        showpoint           & 总是输出小数点                                                                                                                                                                                                                                                                   \\
        *noshowpoint        & 只有当小数部分存在时才显示小数点                                                                                                                                                                                                                                                 \\
        showpos             & 在非负数值中显示 +                                                                                                                                                                                                                                                               \\
        *noshowpos          & 在非负数值中不显示 +                                                                                                                                                                                                                                                             \\
        *skipws             & 输入时跳过空白字符                                                                                                                                                                                                                                                               \\
        noskipws            & 输入时不跳过空白字符                                                                                                                                                                                                                                                             \\
        uppercase           & 十六进制数中使用 A~E。若输出前缀，则前缀输出 0X，科学计数法中输出 E                                                                                                                                                                                                              \\
        *nouppercase        & 十六进制数中使用 a~e。若输出前缀，则前缀输出 0x，科学计数法中输出 e。                                                                                                                                                                                                            \\
        internal            & 数值的符号（正负号）在指定宽度内左对齐，数值右对 齐，中间由填充字符填充。                                                                                                                                                                                                        \\
        \bottomrule
    \end{tabular}
    \label{tab:stream_op}
\end{table}
使用这些控制流的方法是将算子用\bashinline{<<}和cout连用。例如：\mint{cpp}{cout << hex << 12 << "," << 24;} 这条语句的作用是指定以十六进制形式输出后面两个数，因此输出结果是：
\bashinline{c, 18}
\subsection{setiosflags() 操作}
setiosflags() 操作实际上是一个库函数，它以一些标志作为参数，这些标志可以是在 iostream 头文件中定义的\tabref{tab:stream_set}几种取值，它们的含义和同名算子一样。
\begin{table}[!htbp]
    \centering
    \caption{setiosflags() 可以使用的标志及其说明}
    \begin{tabular}{p{0.2\textwidth}p{0.8\textwidth}}
        \toprule
        标 志           & 作 用                                                         \\
        \midrule
        ios::left       & 输出数据在本域宽范围内向左对齐                                \\
        ios::right      & 输出数据在本域宽范围内向右对齐                                \\
        ios::internal   & 数值的符号位在域宽内左对齐，数值右对齐，中间由填充字符填充    \\
        ios::dec        & 设置整数的基数为 10                                           \\
        ios::oct        & 设置整数的基数为 8                                            \\
        ios::hex        & 设置整数的基数为 16                                           \\
        ios::showbase   & 强制输出整数的基数（八进制数以 0 开头，十六进制数以 0x 打头） \\
        ios::showpoint  & 强制输出浮点数的小点和尾数 0                                  \\
        ios::uppercase  & 在以科学记数法格式 E 和以十六进制输出字母时以大写表示         \\
        ios::showpos    & 对正数显示“+”号                                               \\
        ios::scientific & 浮点数以科学记数法格式输出                                    \\
        ios::fixed      & 浮点数以定点格式（小数形式）输出                              \\
        ios::unitbuf    & 每次输出之后刷新所有的流                                      \\
        ios::stdio      & 每次输出之后清除 stdout, stderr                               \\
    \end{tabular}
    \label{tab:stream_set}
\end{table}
这些标志实际上都是仅有某比特位为 1，而其他比特位都为 0 的整数。多个标志可以用|运算符连接，表示同时设置。例如：
\begin{cpp}
    cout << setiosflags(ios::scientific|ios::showpos) << 12.34;
\end{cpp}
输出结果是：\bashinline{+1.234000e+001}

设置志时应该用 resetiosflags 清除原先的标志。例如下面三条语句：
\cppfile[firstline=6,lastline=10]{code/stream/ex13/main.cpp}
输出结果是：
\begin{bash}
    12.340000
    +1.234000e+001
    1.234000e+001
\end{bash}
关于流操纵算子的使用，来看下面的程序。
\cppfile{code/stream/ex14/main.cpp}
程序的输出结果是：
\begin{bash}
    不同进制输出：
    十六进制：101 八进制：401 十进制：257
    浮点数尾数显示：
    默认有效位数6位：3.14159有效位数10：3.141592503
    使用科学计数法保留小数点后5位:3.14159e+00
    显示正负符号,输出位数20，不足用*替换:********+3.14159e+00
    左对齐：+3.14159e+00********
    右对齐：********+3.14159e+00
    居中对齐：*********3.14159e+00
    输出默认：3.14159e+00
\end{bash}
需要注意的是，setw() 算子所起的作用是一次性的，即只影响下一次输出。每次需要指定输出宽度时都要使用 setw()。因此可以看到，第 10 行的输出因为没有使用 setw()，输出的宽度就不再是前面指定的 12 个字符。在读入字符串时，setw() 还能影响 cin 的行为。例如下面的程序：
\begin{cpp}
    #include <iostream>
    #include <iomanip>
    using namespace std;
    int main()
    {
            string s1, s2;
            cin >> setw(4) >> s1 >> setw(3) >> s2;
            cout << s1 << "," << s2 << endl;
            return 0;
        }
\end{cpp}
输入：
\begin{bash}
    1234567890
\end{bash}

程序的输出结果是：
\begin{bash}
    1234,567
\end{bash}

说明setw(4)使得读入 s1 时，只读入 4 个字符，其后的setw(3)使得读入 s2 时只读入 3 个字符。setw() 用于 cin 时，同样只影响下一次的输入。ostream 类有一些成员函数，通过 cout 调用它们也能用于控制输出的格式，其作用和流操纵算子相同，如\tabref{tab:stream_member}所示。
\begin{table}[!htbp]
    \centering
    \caption{ostream 类的成员函数}
    \begin{tabular}{p{0.2\linewidth}p{0.2\linewidth}p{0.5\linewidth}}
        \toprule
        成员函数     & 作用相同的流操纵算子 & 说明                                                                            \\
        \midrule
        precision(n) & setprecision(n)      & 设置输出浮点数的精度为 n。                                                      \\
        width(w)     & setw(w)              & 指定输出宽度为 w 个字符。                                                       \\
        fill(c)      & setfill (c)          & 在指定输出宽度的情况下，输出的宽度不足时用字符 c 填充（默认情况是用空格填充）。 \\
        setf(flag)   & setiosflags(flag)    & 将某个输出格式标志置为 1。                                                      \\
        unsetf(flag) & resetiosflags(flag)  & 将某个输出格式标志置为 0。                                                      \\
        \bottomrule
    \end{tabular}
    \label{tab:stream_member}
\end{table}
setf 和 unsetf 函数用到的flag，与 setiosflags 和 resetiosflags 用到的完全相同。
这些成员函数的用法十分简单。例如下面的三行程序：
\begin{cpp}
    cout.setf(ios::scientific);
    cout.precision(8);
    cout << 12.23 << endl;
\end{cpp}
输出结果是：
\begin{bash}
    1.22300000e+001
\end{bash}
\subsection{输入单个字符}
在 C++ 程序中一般用 cout 和输出运算符 \cppinline{<<} 实现输出，cout 流在内存中有相应的缓冲区。但有时用户还有特殊的输出需求，例如只输出一个字符。

ostream 类除了提供上一节介绍过的用于格式控制的成员函数外，还提供了专门用于输出单个字符的成员函数——put()，它的用法为：\cppinline{cout.put(ch);}。ch 为要输出的字符。put() 函数的返回值是 cout 的引用，所以你可以像下面这样连续使用 put()：\mint{cpp}{cout.put(ch1).put(ch2).put(ch3);}

put() 函数的参数可以是字符或字符的 ASCII 代码（也可以是一个整型表达式）。
\begin{cpp}
    cout.put(65 + 32);
    cout.put(97);
\end{cpp}
上面两行代码都输出字符 a，因为 97 是字符 a 的 ASCII 代码。可以在一个语句中连续调用 put() 函数，例如：
\begin{cpp}
    cout.put(71).put(79).put(79). put(68).put('\n');
\end{cpp}
在屏幕上显示GOOD。反向输出字符串：
\cppfile{code/stream/ex6/main.cpp}
运行结果：
\begin{bash}
    dlrow olleh
\end{bash}
除了使用 cout.put() 函数输出一个字符外，还可以用 putchar() 函数输出一个字符。 putchar() 函数是C语言中使用的，在 stdio.h 头文件中定义，C++保留了这个函数，在 iostream 头文件中定义。
\subsection{cin结束判断}
cin 可以用来从键盘输入数据；将标准输入重定向为文件后，cin 也可以用来从文件中读入数据。在输入数据的多少不确定，且没有结束标志的情况下，该如何判断输入数据已经读完了呢？
其实，在控制台中输入特殊的控制字符就表示输入结束了：在 Windows 系统中，通过键盘输入时，按 Ctrl+Z 组合键后再按回车键，就代表输入结束。在 UNIX/Linux/Mac OS 系统中，Ctrl+D 代表输入结束。不管是文件末尾，还是 Ctrl+Z 或者 Ctrl+D，它们都是结束标志；cin 在正常读取时返回 true，遇到结束标志时返回 false，我们可以根据 cin 的返回值来判断是否读取结束。cin 判断控制台（键盘）读取结束。输入若干个正整数，输出其中的最大值，程序该如何编写？
\subsubsection{cin 判断文件读取结束}
如果将标准输入重定向为某个文件，如在程序开始添加freopen("test.txt", "r", stdin);语句，或者不添加上述语句，但是在 Windows 的“命令提示符”窗口中输入：
\begin{bash}
    program < test.txt  //假设编译生成的可执行文件的名字为 program
\end{bash}
在《C++重载\bashinline{<<}和>>》一节中我们提到过 istream 类将\cppinline{>>}重载为成员函数，而且这些成员函数的返回值是 cin 的引用。准确地说，\cppinline{cin>>n}的返回值的确是 istream \& 类型的，而 while 语句中的条件表达式的返回值应该是 bool 类型、整数类型或其他和整数类型兼容的类型，istream \& 显然和整数类型不兼容，为什么\cppinline{while(cin>>n)}还能成立呢？

这是因为，istream 类对强制类型转换运算符 bool 进行了重载，这使得 cin 对象可以被自动转换成 bool 类型。所谓自动转换的过程，就是调用 cin 的 operator bool() 这个成员函数，而该成员函数可以返回某个标志值，该标志值在 cin 没有读到输入结尾时为 true，读到输入结尾后变为 false。对该标志值的设置，在 operator \bashinline{<<}() 成员函数中进行。如果 cin 在读取过程中发生了错误，\cppinline{cin>>n}这样的表达式也会返回 false。例如下面的程序：
\begin{cpp}
    #include <iostream>
    using namespace std;
    int main()
    {
            int n;
            while (cin >> n)
            cout << n << endl;
            return 0;
        }
\end{cpp}
程序本该输入整数，如果输入了一个字母，则程序就会结束。因为，应该读入整数时却读入了字母也算读入出错。
\subsection{读取单个字符}
get() 是 istream 类的成员函数，它有多种重载形式，不过本文只介绍最简单最常用的一种： \cppinline{int get();}。此函数从输入流中读入一个字符，返回值就是该字符的 ASCII 码。如果碰到输入的末尾，则返回值为 EOF。

EOF 是 End of File 的缩写。istream 类中从输入流（包括文件）中读取数据的成员函数，在把输入数据都读取完后再进行读取，就会返回 EOF。EOF 是在 iostream 类中定义的一个整型常量，值为 -1。
\begin{remark}
    get() 函数不会跳过空格、制表符、回车等特殊字符，所有的字符都能被读入。
\end{remark}
例如下面的程序：
\cppfile{code/stream/ex8/main.cpp}
程序运行情况如下：
\begin{bash}
    hello world↙
    hello world
    ^D↙
\end{bash}
↙表示回车键，\^ D表示 Ctrl+D 组合键。程序中的变量 c 应为 int 类型，而不能是 char 类型。在输入流中碰到 ASCII 码等于 0xFF 的字符时，cin.get() 返回 0xFF，0xFF 赋值给 c，此时如果 c 是 char 类型的，那么其值就是 -1（因为符号位为 1 代表负数），即等于 EOF，于是程序就错误地认为输入已经结束。

而在 c 为 int 类型的情况下，将 0xFF 赋值给 c，c 的值是 255（因为符号位为 0，是正数），而非 -1，即除非读到输入末尾，c 的值都不可能是 -1。要将文本文件 test.txt 中的全部内容原样显示出来，程序可以如下编写：
\cppfile{code/stream/ex9/main.cpp}
\subsection{读取整行内容}
getline() 是 istream 类的成员函数，它有如下两个重载版本：
\begin{cpp}
    istream & getline(char* buf, int bufSize);
    istream & getline(char* buf, int bufSize, char delim);
\end{cpp}
第一个版本从输入流中读取 bufSize 个字符到缓冲区 buf，或遇到\bashinline{\n}为止（哪个条件先满足就按哪个执行）。函数会自动在 buf 中读入数据的结尾添加\bashinline{\0}。

第二个版本和第一个版本的区别在于，第一个版本是读到\bashinline{\n}为止，第二个版本是读到 delim 字符为止。\bashinline{\n}或 delim 都不会被读入 buf，但会被从输入流中取走。

这两个函数的返回值就是函数所作用的对象的引用。如果输入流中\bashinline{\n}或 \bashinline{delim} 之前的字符个数达到或超过 bufSize，就会导致读入出错，其结果是：虽然本次读入已经完成，但是之后的读入都会失败。

从输入流中读入一行，可以用第一个版本。用\cppinline{cin >> str}这种写法是不行的，因为此种读法在碰到行中的空格或制表符时就会停止，因此就不能保证 str 中读入的是整行。第一个版本的 getline 函数的用法示例如下：
\cppfile{code/stream/ex10/main.cpp}
程序的运行过程如下：
\begin{bash}
    123459
\end{bash}
在上面的输入情况下，程序是正常的。程序运行过程中还可能出现如下情况：
\begin{bash}
    读取到错误字符
    读取到的字符12345
    尝试读取新的字符：
    120
    清除标记！
    重新开始读取：
    9
\end{bash}
第 7 行，读入时因字符串超长导致出错，于是第 12 行并没有从输入流读入 n，n 维持了原来的值 120。第 15 行，调用 istream 的成员函数 clear() 清除 cin 内部的错误标记，此后 cin 又能正常读入了。因此，123459 在第 17 行被读入 n。可以用 getline() 函数的返回值（为 false 则输入结束）来判断输入是否结束。例如，要将文件 test.txt 中的全部内容（假设文件中一行最长有 10 000个字符）原样显示，程序可以如下编写：
\cppfile{code/stream/ex11/main.cpp}
程序每次读入文件中的一行到 szBuf 并输出。szBuf 中不会读入回车符，因此输出 szBuf 后要再输出 endl 以换行。
\subsection{忽略字符}
ignore() 是 istream 类的成员函数，它的原型是： \mint{cpp}{istream & ignore(int n =1, int delim = EOF);}
此函数的作用是跳过输入流中的 n 个字符，或跳过 delim 及其之前的所有字符，哪个条件先满足就按哪个执行。两个参数都有默认值，因此 cin.ignore() 就等效于 cin.ignore(1, EOF)， 即跳过一个字符。该函数常用于跳过输入中的无用部分，以便提取有用的部分。例如，输入的电话号码形式是Tel:63652823，Tel:就是无用的内容。例如下面的程序：
\cppfile{code/stream/ex12/main.cpp}
程序的运行过程可能如下：
\begin{bash}
    abcde34↙
    34
\end{bash}

cin.ignore() 跳过了输入中的前 5 个字符，其余内容被当作整数输入 n 中。该程序的运行过程也可能如下：
\begin{bash}
    ab#34↙
    34
\end{bash}
cin.ignore() 跳过了输入中的 '\#' 及其前面的字符，其余内容被当作整数输入 n 中。

\subsection{查看输入流中的下一个字符}

peek() 是 istream 类的成员函数，它的原型是：\bashinline{int peek();} 此函数返回输入流中的下一个字符，但是并不将该字符从输入流中取走——相当于只是看了一眼下一个字符，因此叫 peek。cin.peek() 不会跳过输入流中的空格、回车符。在输入流已经结束的情况下，cin.peek() 返回 EOF。在输入数据的格式不同，需要预先判断格式再决定如何输入时，peek() 就能起到作用。

例题：编写一个日期格式转换程序，输入若干个日期，每行一个，要求全部转换为“mm-dd-yyyy”格式输出。输入的日期格式可以是“2011.12.24”（中式格式），也可以是“Dec 24 2011”（西式格式）。要求该程序对于以下输入数据：
\begin{bash}
    Dec 3 1990
    2011.2.3
    458.12.1
    Nov 4 1998
    Feb 12 2011
\end{bash}
输出结果应为：
\begin{bash}
    12-03-1990
    02-03-2011
    12-01-0458
    11-04-1998
    02-12-2011
\end{bash}
输入数据中的 Ctrl+D 略去不写，因为输入数据也可能来自于文件。

编写这个程序时，如果输入的是中式格式，就用 cin>>year（假设 year 是 int 类型变量）读取年份，然后再读取后面的内容；如果输入是西式格式，就用 cin>>sMonth（假设 sMonth 是 string 类型对象）读取月份，然后读取后面的内容。可是，如果没有将数据从输入流中读取出来，就无法判断输入到底是哪种格式。即便用 cin.get() 读取一个字符后再作判断，也很不方便。例如，在输入为2011.12.24的情况下，读取第一个字符2后就知道是格式一，问题是输入流中的已经被读取了，剩下的表示年份的部分只有011，如何将这个011和前面读取的2奏成一个整数 2011，也是颇费周折的事情。使用 peek() 函数很容易解决这个问题。示例程序如下：
\begin{cpp}
    #include <iostream>
    #include <iomanip>
    #include <string>
    using namespace std;
    string Months[12] = { "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug", "Sep","Oct","Nov","Dec" };
    int main()
    {
            int c;
            while((c = cin.peek()) != EOF) { //取输入流中的第一个字符进行查看
                    int year,month,day;
                    if(c >= 'A' && c <= 'Z') { //美国日期格式
                            string sMonth;
                            cin >> sMonth >> day >> year;
                            for(int i = 0;i < 12; ++i)  //查找月份
                            if(sMonth == Months[i]) {
                                    month = i + 1;
                                    break;
                                }
                        }
                    else { //中国日期格式
                            cin >> year ;
                            cin.ignore() >> month ; //用ignore跳过 "2011.12.3"中的'.'
                            cin.ignore() >> day;
                        }
                    cin.ignore();   //跳过行末 '\n'
                    cout<< setfill('0') << setw(2) << month ;//设置填充字符'\0'，输出宽度2
                    cout << "-" << setw(2) << day << "-" << setw(4) << year << endl;
                }
            return 0;
        }
\end{cpp}
istream 还有一个成员函数 \cppinline{istream & putback(char c)}，可以将一个字符插入输入流的最前面。对于上面的例题，也可以在用 get() 函数读取一个字符并判断是中式格式还是西式格式时，将刚刚读取的字符再用 putback() 成员函数放回流中，然后再根据判断结果进行不同方式的读入。
\subsection{输入输出错误}
当处理输入输出时，我们必须预计到其中可能发生的错误并给出相应的处理措施。当我们输入时，可能会由于人的失误（错误理解了指令、打字错误等）、文件格式不符、错误估计了情况等原因造成读取失败。当我们输出时，如果输出设备不可用、队列满或者发生了故障等，都会导致写入失败。发生输入输出错误的可能情况是无限的！但 C++ 将所有可能的情况归结为四类，称为流状态（stream state）。每种流状态都用一个 iostate 类型的标志位来表示。

\begin{table}[!htbp]
    \centering
    \caption{流状态对应的标志位}
    \begin{tabular}{p{0.2\textwidth}p{0.7\textwidth}}
        \toprule
        标志位    & 意义                                                                                                                                      \\
        \midrule
        badbit    & 发生了（或许是物理上的）致命性错误，流将不能继续使用。                                                                                    \\
        eofbit    & 输入结束（文件流的物理结束或用户结束了控制台流输入，例如用户按下了 Ctrl+Z 或 Ctrl+D 组合键。                                              \\
        failbit   & I/O 操作失败，主要原因是非法数据（例如，试图读取数字时遇到字母）。流可以继续使用，但会设置 failbit 标志。                                 \\
        goodbit   & 一切止常，没有错误发生，也没有输入结束。                                                                                                  \\
        ios\_base & 类定义了以上四个标志位以及 iostate 类型，但是 ios 类又派生自 ios\_base 类，所以可以使用 ios::failbit 代替 ios\_base::failbit 以节省输入。 \\
        \bottomrule
    \end{tabular}
    \label{tab:stream_flag}
\end{table}
一旦流发生错误，对应的标志位就会被设置，我们可以通过下表列出的函数检测流状态。
\begin{table}[H]
    \centering
    \caption{C++流状态检测函数及其说明}
    \begin{tabular}{p{0.2\textwidth}p{0.2\textwidth}p{0.6\textwidth}}
        \toprule
        检测函数 & 对应的标志位 & 说明                                                             \\
        \midrule
        good()   & goodbit      & 操作成功，没有发生任何错误。                                     \\
        eof()    & eofbit       & 到达输入末尾或文件尾。                                           \\
        fail()   & failbit      & 发生某些意外情况（例如，我们要读入一个数字，却读入了字符 'x'）。 \\
        bad()    & badbit       & 发生严重的意外（如磁盘读故障）。                                 \\
    \end{tabular}
    \label{tab:stream_status}
\end{table}
不幸的是，fail() 和 bad() 之间的区别并未被准确定义，程序员对此的观点各种各样。但是，基本的思想很简单：
如果输入操作遇到一个简单的格式错误，则使流进入 fail() 状态，也就是假定我们（输入操作的用户）可以从错误中恢复。如果错误真的非常严重，例如发生了磁盘故障，输入操作会使得流进入 bad() 状态。也就是假定面对这种情况你所能做的很有限，只能退出输入。以上观点导致如下逻辑：
\begin{cpp}
    int i = 0;
    cin >> i;
    if(!cin){  //只有输入操作失败，才会跳转到这里
            if(cin.bad()){  //流发生严重故障，只能退出函数
                    error("cin is bad!");  //error是自定义函数，它抛出异常，并给出提示信息
                }
            if(cin.eof()){  //检测是否读取结束
                    //TODO:
                }
            if(cin.fail()){  //流遇到了一些意外情况
                    cin.clear(); //清除/恢复流状态
                    //TODO:
                }
        }
\end{cpp}
!cin 可以理解为“cin 不成功”或者“cin 发生了某些错误”或者“ cin 的状态不是 good()”， 这与“操作成功”正好相反。

请注意我们在处理 fail() 时所使用的 cin.clear()。当流发生错误时，我们可以进行错误恢复。为了恢复错误，我们显式地将流从 fail() 状态转移到其他状态，从而可以继续从中读取字符。clear() 就起到这样的作用——执行 cin.clear() 后，cin 的状态就变为 good()。

下面是一个如何使用流状态的例子。假定我们要读取一个整数序列并存入 vector 中，字符*或“文件尾”表示序列结束。Windows 平台按下 Ctrl+Z 组合键，再按下回车键表示到达文件末尾；类Unix系统按下 Ctrl+D 组合键表示到达文件末尾。上述功能可通过如下函数来实现：
\begin{cpp}
    //从 ist 中读入整数到 v 中，直到遇到 eof() 或终结符
    void fill_vector(istream& ist, vector<int>& v, char terminator){
            for( int i; ist>>i; ) v.push_back(i);
            //正常情况
            if(ist.eof()) return;  //发现到了文件尾，正确，返回
            //发生严重错误，只能退出函数
            if (ist.bad()){
                    error("cin is bad!");  //error是自定义函数，它抛出异常，并给出提示信息
                }
            //发生意外情况
            if (ist.fail()) {  //最好清除混乱，然后汇报问题
                    ist.clear();  //清除流状态
                    //检测下一个字符是否是终结符
                    char c;
                    ist>>c;  //读入一个符号，希望是终结符
                    if(c != terminator) { // 非终结符
                            ist.unget(); //放回该符号
                            ist.clear(ios_base::failbit);  //将流状态设置为 fail()
                        }
                }
        }
\end{cpp}
如果发生了 fail()，我们尝试检测下一个字符是否是结束符：如果是，那么就完整得读取了数据，使用 clear() 恢复状态就可以；如果不是，我们就没有办法处理了，所以将状态重新设置为 fail()，以期望 fill\_vector() 的调用者（上层函数）有能力处理。

我们通过调用 ist.clear(ios\_base::failbit) 来将流状态设置为 fail()。对照简单的clear()，带参数的用法有些令人迷惑：当 clear() 带参数时，参数中所指出的 iostream 状态位会被置位（进入相应状态)，而未指出的状态位会被复位。通过将流状态设置为 fail()，我们表明遇到了一个格式错误，而不是一个更为严重的问题。

可以用 unget() 将字符放回 ist，以便 fill\_vector() 的调用者可能使用该字符。unget() 函数是 putback() 的简化版本，它依赖于流对象记住最后一个字符是什么，所以在这里可以不用考虑它的用法。

如果 fill\_vector() 的调用者想知道是什么原因终止了输入，那么可以检测流是处于 fail() 还是 eof() 状态。当然也可以捕获 error() 抛出的 runtime\_error 异常，但当 istream 处于 bad() 状态时，继续获取数据是不可能的。大多数的调用者无须为此烦恼。因为这意味着，几乎在所有情况下，对于 bad() 状态，我们所能做的只是抛出一个异常。

简单起见，可以让 istream 帮我们抛出这个异常。
\begin{cpp}
    //当 ist 出现问题时拋出异常
    ist.exceptions(ist.exceptions() | ios_base:: badbit);
\end{cpp}
这样的写法也许看起来有些奇怪，但结果却很简单，当此语句执行时，如果 ist 处于 bad() 状态，它会抛出一个标准库异常 ios\_base::failure。在一个程序中，我们只需要调用 exceptions() 一次。这允许我们简化关联于 ist 的所有输入过程，同时忽略对 bad() 的处理：
\begin{cpp}
    //从ist中读入整数到v中，直到遇到eof()或终结符
    void fill_vector(istream& ist, vector<int>& v, char terminator){
            ist.exceptions(ist.exceptions() | ios_base:: badbit);
            for (int i; ist>>i; ) v.push_back(i);
            if (ist.eof()) return;  //发现到了文件尾
            //不是good()，不是bad()，不是eof()，ist的状态一定是fail()
            ist.clear();  //清除流状态
            char c;
            ist>>c;    //读入一个符号，希望是终结符
            if (c != terminator) { //不是终结符号，一定是失败了
                    ist.unget();    //也许程序调用者可以使用这个符号
                    ist.clear(ios_base::failbit); //将流状态设置为 fail()
                }
        }
\end{cpp}
这里使用了 ios\_base，它是 iostream 的一部分，包含了对常量如 badbit 的定义、异常如 failure 的定义，以及其他一些有用的定义。可以通过::操作符来使用它们，例如 ios\_ base::badbit。

我们无须如此深入地讨论 iostream 库的细节，若要学习 iostream的所有内容，可能需要一门完整的课程。例如，iostream 可以处理不同的字符集，实现不同的缓冲策略，还包含一些工具，能按不同语言的习惯格式化货币金额的输入输出。我们曾经收到过一份关于乌克兰货币输入输出格式的错误报告。如果需要了解更多 iostream 库的内容，可以参考 Stroustrup 的《The C++ Programming Language》和 Langer 的《Standard C++ IOStreams and Locales》。

与 istream一样，ostream 也有四个状态：good()、fail()、eof() 和 bad()。不过，对于本教程的读者来说，输出错误要比输入错误少得多，因此通常不对 ostream 进行状态检测。如果程序运行环境中输出设备不可用、队列满或者发生故障的概率很高，我们就可以像处理输入操作那样，在每次输出操作之后都检测其状态。
\subsection{管理缓冲区}
每个输出流都管理一个缓冲区，用来保存程序读写的数据。例如，如果执行下而的代码：
\mint{cpp}{cout << "hello world";}
文本串可能立即打印出来，但也有可能被操作系统保存在缓冲区中，随后再打印。有了缓冲机制，操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时，允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。导致缓冲刷新（数据真正写到输出设备或文件）的原因有很多：
\begin{itemize}
    \item 程序正常结束，作为 main() 函数的 return 操作的一部分，缓冲刷新被执行。
    \item 缓冲区满时，需要刷新缓冲，而后新的数据才能继续写入缓冲区。
    \item 我们可以使用操纵符如 endl 来显式刷新缓冲区。
    \item 在每个输出操作之后，我们可以用操作符 unitbuf 设置流的内部状态，来清空缓冲区。默认情况下，对 cerr 是设置 unitbuf 的，因此写到 cerr 的内容都是立即刷新的。
    \item 一个输出流可能被关联到另一个流。在这种情况下，当读写被关联的流时，关联到的流的缓冲区会被刷新。例如，默认情况下，cin 和 cerr 都关联到 cout。因此，读 cin 或写 cerr 都会导致 cout 的缓冲区被刷新。
\end{itemize}
\subsection{刷新输出缓冲区}
我们已经使用过操作符 endl，它完成换行并刷新缓冲区的工作。IO 库中还有两个类似的操纵符：
flush 和 ends。flush 刷新缓冲区，但不输出任何额外的字符；
ends向缓冲区插入一个空字符，然后刷新缓冲区。

请看下面的例子：
\begin{cpp}
    cout << "hi!" << endl;  //输出hi和一个换行，然后刷新缓冲区
    cout << "hi!" << flush;  //输出hi，然后刷新缓冲区，不附加任何额外字符
    cout << "hi!" << ends;  //输出hi和一个空字符，然后刷新缓冲区
\end{cpp}
\subsubsection{unitbuf 操作符}
如果想在每次输出操作后都刷新缓冲区，我们可以使用 unitbuf 操作符，它告诉流在接下来的每次写操作之后都进行一次 flush 操作。而 nounitbuf 操作符则重置流， 使其恢复使用正常的系统管理的缓冲区刷新机制：
\begin{cpp}
    cout << unitbuf;  //所有输出操作后都会立即刷新缓冲区
    //任何输出都立即刷新，无缓冲
    cout << nounitbuf;  //回到正常的缓冲方式
\end{cpp}
\begin{Warm}{输出警告}
    如果程序崩溃，输出缓冲区不会被刷新。如果程序异常终止，输出缓冲区是不会被刷新的。当一个程序崩溃后，它所输出的数据很可能停留在输出缓冲区中等待打印。
\end{Warm}
当调试一个已经崩溃的程序时，需要确认那些你认为已经输出的数据确实已经刷新了。否则，可能将大量时间浪费在追踪代码为什么没有执行上，而实际上代码已经执行了，只是程序崩溃后缓冲区没有被刷新，输出数据被挂起没有打印而已。

当一个输入流被关联到一个输出流时，任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将 cout 和 cin 关联在一起，因此下面语句：
\cppinline{cin >> ival;} 导致 cout 的缓冲区被刷新。

交互式系统通常应该关联输入流和输出流。这意味着所有输出，包括用户提示信息，都会在读操作之前被打印出来。tie() 函数可以用来绑定输出流，它有两个重载的版本：
\begin{cpp}
    ostream* tie ( ) const;  //返回指向绑定的输出流的指针。
    ostream* tie ( ostream* os );  //将 os 指向的输出流绑定的该对象上，并返回上一个绑定的输出流指针。
\end{cpp}
第一个版本不带参数，返冋指向出流的指针。如果本对象当前关联到一个输出流，则返回的就是指向这个流的指针，如果对象未关联到流，则返回空指针。

tie() 的第二个版本接受一个指向 ostream 的指针，将自己关联到此 ostream，即：x.tie(\&o) 将流 x 关联到输出流 o。我们既可以将一个 istream 对象关联到另一个 ostream，也可以将一个 ostream 关联到另一个 ostream：
\begin{cpp}
    cin.tie(&cout);  //仅仅是用来展示，标准库已经将 cin 和 cout 关联在一起
    //old_tie 指向当前关联到 cin 的流（如果有的话）
    ostream *old_tie = cin.tie(nullptr);  // cin 不再与其他流关联
    //将 cin 与 cerr 关联，这不是一个好主意，因为 cin 应该关联到 cout
    cin.tie(&cerr);  //读取 cin 会刷新 cerr 而不是 cout
    cin.tie(old_tie);  //重建 cin 和 cout 间的正常关联
\end{cpp}
在这段代码中，为了将一个给定的流关联到一个新的输出流，我们将新流的指针传递给了 tie()。为了彻底解开流的关联，我们传递了一个空指针。每个流同时最多关联到一个流， 但多个流可以同时关联到同一个ostream。
\section{文件操作}
内存中存放的数据在计算机关机后就会消失。要长久保存数据，就要使用硬盘、光盘、U 盘等设备。为了便于数据的管理和检索，引入了“文件”的概念。一篇文章、一段视频、一个可执行程序，都可以被保存为一个文件，并赋予一个文件名。操作系统以文件为单位管理磁盘中的数据。成千上万个文件如果不加分类放在一起，用户使用起来显然非常不便，因此又引入了树形目录（目录也叫文件夹）的机制，可以把文件放在不同的文件夹中，文件夹中还可以嵌套文件夹，这就便于用户对文件进行管理和使用，正如 Windows 的资源管理器呈现的那样。一般来说，文件可分为文本文件、视频文件、音频文件、图像文件、可执行文件等多种类别，这是从文件的功能进行分类的。从数据存储的角度来说，所有的文件本质上都是一样的，都是由一个个字节组成的，归根到底都是 0、1 比特串。不同的文件呈现出不同的形态（有的是文本，有的是视频等等），这主要是文件的创建者和解释者（使用文件的软件）约定好了文件格式。

所谓“格式”，就是关于文件中每一部分的内容代表什么含义的一种约定。例如，常见的纯文本文件（也叫文本文件，扩展名通常是“.txt”），指的是能够在 Windows 的“记事本”程序中打开，并且能看出是一段有意义的文字的文件。文本文件的格式可以用一句话来描述：文件中的每个字节都是一个可见字符的 ASCII 码。

除了纯文本文件外，图像、视频、可执行文件等一般被称作“二进制文件”。二进制文件如果用文本编辑软件（如：“记事本”）打开，看到的是一片乱码。所谓“文本文件”和“二进制文件”，只是约定俗成的、从计算机用户角度出发进行的分类，并不是计算机科学的分类。因为从计算机科学的角度来看，所有的文件都是由二进制位组成的，都是二进制文件。文本文件和其他二进制文件只是格式不同而已。实际上，只要规定好格式，而且不怕浪费空间，用文本文件一样可以表示图像、声音、视频甚至可执行程序。简单地说，如果约定用字符 '1'、'2'、...、'7' 表示七个音符，那么由这些字符组成的文本文件就可以被遵从该约定的音乐软件演奏成一首曲子。下面再看一个用文本文件表示一幅图像的例子：一幅图像实际上就是一个由点构成的矩阵，每个点可以有不同的颜色，称为像素。有的图像是 256 色的，有的是 32 位真彩色（即一 个像素的颜色用一个 32 位的整数表示）的。以 256 色图像为例，可以用 0~255 这 256 个数代表 256 种颜色，那么每个像素就可以用一个数来表示。再约定文件开始的两个数代表图像的宽度和高度（以像素为单位），则以下文本文件就可以表示一幅宽度为 6 像素、高度为 4 像素的 256 色图像：
\begin{bash}
    6 4
    24 0 38 129 4 154
    12 73 227 40 0 0
    12 173 127 20 0 0
    21 73 87 230 1 0
\end{bash}
这个“文本图像”文件的格式可以描述为：第一行的两个数分别代表水平方向的像素数目和垂直方向的像素数目，此后每行代表图像的一行像素，一行中的每个数对应于一个像素，表示其颜色。理解这一格式的图像处理软件就可以把上述文本文件呈现为一幅图像。视频是由每秒 24 幅图像组成的，因此用文本文件也可以表示视频。

上面用文本文件表示图像的方法是非常低效的，浪费了太多的空间。文件中大量的空格是一种浪费。另外，常常要用 2 个甚至 3 个字符来表示一个像素，也造成大量浪费，因为用一个字节就足以表示 0~255 这 256 个数。因此，可以约定一个更节省空间的格式来表示一个 256 色的图像，此种文件格式的描述如下：文件中的第 0 和第 1 个字节是整数 n，代表图像的宽度（2 字节的 n 的取值范围是 0~65 535，说明图像最多只能是 65 535 个像素宽），第 2 和第 3 个字节代表图像的高度。接下来，每 n 个字节表示图像的一行像素，其中每个字节对应于一个像素的颜色。用这种格式存储 256 色图像，比用上面的文本格式存储图像能够大大节省空间。在“记事本”程序中打开它，看到的就会是乱码，这个图像文件也就是所谓的“二进制文件”。

真正的图像文件、音频文件、视频文件的格式都比较复杂，有的还经过了压缩，但只要文件的制作软件和解读软件（如图像查看软件，音频、视频播放软件）遵循相同的格式约定，用户就可以在文件解读软件中看到文件的内容。
\subsection{文件打开}
在对文件进行读写操作之前，先要打开文件。打开文件有以下两个目的：
\begin{enumerate}
    \item 通过指定文件名，建立起文件和文件流对象的关联，以后要对文件进行操作时，就可以通过与之关联的流对象来进行。
    \item 指明文件的使用方式。使用方式有只读、只写、既读又写、在文件末尾添加数据、以文本方式使用、以二进制方式使用等多种。
\end{enumerate}
打开文件可以通过以下两种方式进行：
\begin{enumerate}
    \item 调用流对象的 open 成员函数打开文件。
    \item 定义文件流对象时，通过构造函数打开文件。
\end{enumerate}
关闭文件时，调用文件流对象的 close 成员函数即可。
\subsection{使用 open 函数打开文件}
先看第一种文件打开方式。以 ifstream 类为例，该类有一个 open 成员函数，其他两个文件流类也有同样的 open 成员函数：\mint{cpp}{void open(const char* szFileName, int mode)}
第一个参数是指向文件名的指针，第二个参数是文件的打开模式标记。

文件的打开模式标记代表了文件的使用方式，这些标记可以单独使用，也可以组合使用。\tabref{tab:open_mode}列出了各种模式标记单独使用时的作用，以及常见的两种模式标记组合的作用。
\begin{table}[H]
    \centering
    \caption{文件打开模式标记}
    \begin{tabular}{p{0.2\textwidth}p{0.2\textwidth}p{0.5\textwidth}}
        \toprule
        模式标记                        & 适用对象                  & 作用                                                                                                                   \\
        \midrule
        ios::in                         & ifstream fstream          & 打开文件用于读取数据。如果文件不存在，则打开出错。                                                                     \\
        ios::out                        & ofstream fstream          & 打开文件用于写入数据。如果文件不存在，则新建该文件；如果文件原来就存在，则打开时清除原来的内容。                       \\
        ios::app                        & ofstream fstream          & 打开文件，用于在其尾部添加数据。如果文件不存在，则新建该文件。                                                         \\
        ios::ate                        & ifstream                  & 打开一个已有的文件，并将文件读指针指向文件末尾如果文件不存在，则打开出错。                                             \\
        ios:: trunc                     & ofstream                  & 单独使用时与 ios::out 相同。                                                                                           \\
        ios::binary                     & ifstream ofstream fstream & 以二进制方式打开文件。若不指定此模式，则以文本模式打开。                                                               \\
        ios::in | ios::out              & fstream                   & 打开已存在的文件，既可读取其内容，也可向其写入数据。文件刚打开时，原有内容保持不变。如果文件不存在，则打开出错。       \\
        ios::in | ios::out              & ofstream                  & 打开已存在的文件，可以向其写入数据。文件刚打开时，原有内容保持不变。如果文件不存在，则打开出错。                       \\
        ios::in | ios::out | ios::trunc & fstream                   & 打开文件，既可读取其内容，也可向其写入数据。如果文件本来就存在，则打开时清除原来的内容；如果文件不存在，则新建该文件。 \\
        \bottomrule
    \end{tabular}
    \label{tab:open_mode}
\end{table}
ios::binary 可以和其他模式标记组合使用，例如：
\cppinline{ios::in | ios::binary} 表示用二进制模式，以读取的方式打开文件。
\cppinline{ios::out | ios::binary} 表示用二进制模式，以写入的方式打开文件。文本方式与二进制方式打开文件的区别其实非常微小。一般来说，如果处理的是文本文件，那么用文本方式打开会方便一些。但其实任何文件都可以以二进制方式打开来读写。

在流对象上执行 open 成员函数，给出文件名和打开模式，就可以打开文件。判断文件打开是否成功，可以看“对象名”这个表达式的值是否为 true，如果为 true，则表示文件打开成功。下面的程序演示了如何打开文件：
\cppfile{code/stream/ex15/main.cpp}
调用 open 成员函数时，给出的文件名可以是全路径也可以是相对路径，代码第 8 行的表示的是相对路径。

\subsection{使用流类的构造函数打开文件}
定义流对象时，在构造函数中给出文件名和打开模式也可以打开文件。以 ifstream 类为例，它有如下构造函数：
\begin{cpp}
    ifstream::ifstream (const char* szFileName, int mode = ios::in, int);
\end{cpp}
第一个参数是指向文件名的指针；第二个参数是打开文件的模式标记，默认值为ios::in; 第三个参数是整型的，也有默认值，一般极少使用。在定义流对象时打开文件的示例程序如下（用流类的构造函数打开文件）：
\cppfile{code/stream/ex16/main.cpp}
\subsection{文件读取写入}
使用文件流对象打开文件后，文件就成为一个输入流或输出流。对于文本文件，可以使用 cin、cout 读写。流的成员函数和流操纵算子同样适用于文件流，因为 ifstream 是 istream 的派生类，ofstream 是 ostream 的派生类，fstream 是 iostream 的派生类，而 iostream 又是从 istream 和 ostream 共同派生而来的。
\begin{example}[编写一个程序，将文件 test\_in 中的整数排序后输出到 test\_out]

    例如，若 test\_in 的内容为：
    \begin{bash}
        4 5 2 6
        9 7 3
    \end{bash}

    则执行本程序后，生成的 test\_out 的内容为：
    \begin{bash}
        9 7 6 5 4 3 2
    \end{bash}

    示例程序如下：
    \cppfile{code/stream/ex17/main.cpp}
\end{example}
程序中如果用二进制方式打开文件，结果毫无区别。
\subsection{二进制文件的读写}
我们先来说一下为什么要使用二进制文件，它比文本文件有哪些好处。用文本方式存储信息不但浪费空间，而且不便于检索。例如，一个学籍管理程序需要记录所有学生的学号、姓名、年龄信息，并且能够按照姓名查找学生的信息。程序中可以用一个类来表示学生：
\cppfile[firstline=7,lastline=15]{code/stream/ex18/main.cpp}
如果用文本文件存储学生的信息，文件可能是如下样子：
\begin{cpp}
    Jack 20
    Alice 18
\end{cpp}

这种存储方式不但浪费空间，而且查找效率低下。因为每个学生的信息所占用的字节数不同，所以即使文件中的学生信息是按姓名排好序的，要用程序根据名字进行查找仍然没有什么好办法，只能在文件中从头到尾搜索。如果把全部的学生信息都读入内存并排序后再查找，当然速度会很快，但如果学生数巨大，则把所有学生信息都读人内存可能是不现实的。

可以用二进制的方式来存储学生信息，即把 CStudent 对象直接写入文件。在该文件中，每个学生的信息都占用 sizeof(CStudent) 个字节。对象写入文件后一般称作“记录”。本例中，每个学生都对应于一条记录。该学生记录文件可以按姓名排序，则使用折半查找的效率会很高。

读写二进制文件不能使用前面提到的类似于 cin、cout 从流中读写数据的方法。这时可以调用 ifstream 类和 fstream 类的 read 成员函数从文件中读取数据，调用 ofstream 和 fstream 的 write 成员函数向文件中写入数据。
用 ostream::write 成员函数写文件
ofstream 和 fstream 的 write 成员函数实际上继承自 ostream 类，原型如下：
\mint{cpp}{ostream & write(char* buffer, int count);}

该成员函数将内存中 buffer 所指向的 count 个字节的内容写入文件，返回值是对函数所作用的对象的引用，如 obj.write(...) 的返回值就是对 obj 的引用。

write 成员函数向文件中写入若干字节，可是调用 write 函数时并没有指定这若干字节要写入文件中的什么位置。那么，write 函数在执行过程中到底把这若干字节写到哪里呢？答案是从文件写指针指向的位置开始写入。

文件写指针是 ofstream 或 fstream 对象内部维护的一个变量。文件刚打开时，文件写指针指向文件的开头（如果以 ios::app 方式打开，则指向文件末尾），用 write 函数写入 n 个字节，写指针指向的位置就向后移动 n 个字节。

下面的程序从键盘输入几名学生的姓名和年龄，并以二进制文件形式存储，成为一个学生记录文件 students.dat。

例子，用二进制文件保存学生记录：
\cppfile{code/stream/ex18/main.cpp}
则形成的 students.dat 为 72 字节，用文本编辑程序打开呈现乱码。

第 24 行指定文件的打开模式是 ios::out|ios::binary，即以二进制写模式打开。在 Windows平台中，用二进制模式打开是必要的，否则可能出错。第 26 行将 s 对象写入文件。s 的地址就是要写入文件的内存缓冲区的地址。但是 \&s 不是 char * 类型，因此要进行强制类型转换。

第 28 行，文件使用完毕一定要关闭，否则程序结束后文件的内容可能不完整。用 istream::read 成员函数读文件。ifstream 和 fstream 的 read 成员函数实际上继承自 istream 类，原型如下：
\mint{cpp}{istream & read(char* buffer, int count);}

该成员函数从文件中读取 count 个字节的内容，存放到 buffer 所指向的内存缓冲区中，返回值是对函数所作用的对象的引用。如果想知道一共成功读取了多少个字节（读到文件尾时，未必能读取 count 个字节），可以在 read 函数执行后立即调用文件流对象的 gcount 成员函数，其返回值就是最近一次 read 函数执行时成功读取的字节数。gcount 是 istream 类的成员函数，原型如下：
\mint{cpp}{int gcount();}

read 成员函数从文件读指针指向的位置开始读取若干字节。文件读指针是 ifstream 或 fstream 对象内部维护的一个变量。文件刚打开时，文件读指针指向文件的开头（如果以ios::app 方式打开，则指向文件末尾），用 read 函数读取 n 个字节，读指针指向的位置就向后移动 n 个字节。因此，打开一个文件后连续调用 read 函数，就能将整个文件的内容读取出来。下面的程序将前面创建的学生记录文件 students.dat 的内容读出并显示。
\cppfile[firstline=32,lastline=43]{code/stream/ex18/main.cpp}
程序的输出结果是：
\begin{bash}
    Readed 40 Bytes
    Name: Jack Age:20
    Readed 40 Bytes
    Name: Alice Age:18
\end{bash}
第 35 行，判断文件是否已经读完的方法和 while(cin>>n) 类似，归根到底都是因为 istream 类重载了 bool 强制类型转换运算符。第 28 行只是演示 gcount 函数的用法，删除该行对程序运行结果没有影响。

可以用 ifstream 和 fstream 类的 get 成员函数（继承自 istream 类）从文件中一次读取一个字节，也可以用 ofstream 和 fstream 类的 put 成员函数（继承自 ostream 类） 向文件中一次写入一个字节。

\begin{example}[编写一个 mycopy 程序，实现文件复制的功能]。

    用法是在“命令提示符”窗口输入：mycopy 源文件名 目标文件名 就能将源文件复制到目标文件。例如：\bashinline{mycopy src.dat dest.dat}
    即将 src.dat 复制到 dest.dat。如果 dest.dat 原本就存在，则原来的文件会被覆盖。解题的基本思路是每次从源文件读取一个字节，然后写入目标文件。程序如下：
    \cppfile{code/stream/ex19/main.cpp}
\end{example}
文件存放于磁盘中，磁盘的访问速度远远低于内存。如果每次读一个字节或写一个字节都要访问磁盘，那么文件的读写速度就会慢得不可忍受。因此，操作系统在接收到读文件的请求时，哪怕只要读一个字节，也会把一片数据（通常至少是 512 个字节，因为磁盘的一个扇区是 512 B）都读取到一个操作系统自行管理的内存缓冲区中，当要读下一个字节时，就不需要访问磁盘，直接从该缓冲区中读取就可以了。

操作系统在接收到写文件的请求时，也是先把要写入的数据在一个内存缓冲区中保存起来，等缓冲区满后，再将缓冲区的内容全部写入磁盘。关闭文件的操作就能确保内存缓冲区中的数据被写入磁盘。

尽管如此，要连续读写文件时，像 mycopy 程序那样一个字节一个字节地读写，还是不如一次读写一片内存区域快。每次读写的字节数最好是 512 的整数倍。
\subsection{文件指针操作}
在读写文件时，有时希望直接跳到文件中的某处开始读写，这就需要先将文件的读写指针指向该处，然后再进行读写。
ifstream 类和 fstream 类有 seekg 成员函数，可以设置文件读指针的位置；
ofstream 类和 fstream 类有 seekp 成员函数，可以设置文件写指针的位置。

所谓“位置”，就是指距离文件开头有多少个字节。文件开头的位置是 0。

这两个函数的原型如下：
\begin{cpp}
    ostream & seekp (int offset, int mode);
    istream & seekg (int offset, int mode);
\end{cpp}

mode 代表文件读写指针的设置模式，有以下三种选项：
\begin{itemize}
    \item ios::beg：让文件读指针（或写指针）指向从文件开始向后的 offset 字节处。offset 等于 0 即代表文件开头。在此情况下，offset 只能是非负数。
    \item ios::cur：在此情况下，offset 为负数则表示将读指针（或写指针）从当前位置朝文件开头方向移动 offset 字节，为正数则表示将读指针（或写指针）从当前位置朝文件尾部移动 offset字节，为 0 则不移动。
    \item ios::end：让文件读指针（或写指针）指向从文件结尾往前的 |offset|（offset 的绝对值）字节处。在此情况下，offset 只能是 0 或者负数。
\end{itemize}
此外，我们还可以得到当前读写指针的具体位置：
\begin{itemize}
    \item ifstream 类和 fstream 类还有 tellg 成员函数，能够返回文件读指针的位置；
    \item ofstream 类和 fstream 类还有 tellp 成员函数，能够返回文件写指针的位置。
\end{itemize}
这两个成员函数的原型分别为\cppinline{int tellg();}和\cppinline{int tellp();}

要获取文件长度，可以用 seekg 函数将文件读指针定位到文件尾部，再用 tellg 函数获取文件读指针的位置，此位置即为文件长度。

例题：假设学生记录文件 students.dat 是按照姓名排好序的，编写程序，在 students.dat 文件中用折半查找的方法找到姓名为 Jack 的学生记录，并将其年龄改为 20（假设文件很大，无法全部读入内存）。程序如下：
\begin{cpp}
    #include <iostream>
    #include <fstream>
    #include <cstring>
    using namespace std;
    class CStudent
        {
            public:
            char szName[20];
            int age;
        };
    int main()
    {
            CStudent s;
            fstream ioFile("students.dat", ios::in|ios::out);//用既读又写的方式打开
            if(!ioFile) {
                    cout << "error" ;
                    return 0;
                }
            ioFile.seekg(0,ios::end); //定位读指针到文件尾部，
            //以便用以后tellg 获取文件长度
            int L = 0,R; // L是折半查找范围内第一个记录的序号
            // R是折半查找范围内最后一个记录的序号
            R = ioFile.tellg() / sizeof(CStudent) - 1;
            //首次查找范围的最后一个记录的序号就是: 记录总数- 1
            do {
                    int mid = (L + R)/2; //要用查找范围正中的记录和待查找的名字比对
                    ioFile.seekg(mid *sizeof(CStudent),ios::beg); //定位到正中的记录
                    ioFile.read((char *)&s, sizeof(s));
                    int tmp = strcmp( s.szName,"Jack");
                    if(tmp == 0) { //找到了
                            s.age = 20;
                            ioFile.seekp(mid*sizeof(CStudent),ios::beg);
                            ioFile.write((char*)&s, sizeof(s));
                            break;
                        }
                    else if (tmp > 0) //继续到前一半查找
                    R = mid - 1 ;
                    else  //继续到后一半查找
                    L = mid + 1;
                }while(L <= R);
            ioFile.close();
            return 0;
        }
\end{cpp}
在 UNIX/Linux 平台中，用文本方式或二进制方式打开文件没有任何区别。在 UNIX/Linux 平台中，文本文件以\bashinline{\n}（ASCII 码为 0x0a）作为换行符号；而在 Windows 平台中，文本文件以连在一起的\bashinline{\r\n}（\bashinline{\r}的 ASCII 码是 0x0d）作为换行符号。

在 Windows 平台中，如果以文本方式打开文件，当读取文件时，系统会将文件中所有的\bashinline{\r\n}转换成一个字符\bashinline{\n}，如果文件中有连续的两个字节是 0x0d0a，则系统会丢弃前面的 0x0d 这个字节，只读入 0x0a。当写入文件时，系统会将\bashinline{\n}转换成\bashinline{\r\n}写入。

也就是说，如果要写入的内容中有字节为 0x0a，则在写人该字节前，系统会自动先写入一个 0x0d。因此，如果用文本方式打开二进制文件进行读写，读写的内容就可能和文件的内容有出入。
因此，用二进制方式打开文件总是最保险的。
